import calendar
import datetime
import re

re_year = "(?P<years>([1-9]\d{3}|\*)(/[1-9]\d*)?|[1-9]\d{3}-[1-9]\d{3}|[1-9]\d{3}(-[1-9]\d{3})?(,[1-9]\d{3}(-[1-9]\d{3})?)+)"
re_month = "(?P<months>((0?[1-9]|1[012])|\*)(/[1-9]\d*)?|(0?[1-9]|1[012])-(0?[1-9]|1[012])|(0?[1-9]|1[012])(-(0?[1-9]|1[012]))?(,(0?[1-9]|1[012])(-(0?[1-9]|1[012]))?)+)"
re_day = "(?P<days>((0?[1-9]|[12]\d|3[01])|\*)(/[1-9]\d*)?|(0?[1-9]|[12]\d|3[01])-(0?[1-9]|[12]\d|3[01])|(0?[1-9]|[12]\d|3[01])(-(0?[1-9]|[12]\d|3[01]))?(,(0?[1-9]|[12]\d|3[01])(-(0?[1-9]|[12]\d|3[01]))?)+)"
re_hour = "(?P<hours>(([01]?\d|2[0123])|\*)(/[1-9]\d*)?|([01]?\d|2[0123])-([01]?\d|2[0123])|([01]?\d|2[0123])(-([01]?\d|2[0123]))?(,([01]?\d|2[0123])(-([01]?\d|2[0123]))?)+)"
re_minute = "(?P<minutes>([0-5]?\d|\*)(/[1-9]\d*)?|[0-5]?\d-[0-5]?\d|[0-5]?\d(-[0-5]?\d)?(,[0-5]?\d(-[0-5]?\d)?)+)"
re_second = "(?P<seconds>([0-5]?\d|\*)(/[1-9]\d*)?|[0-5]?\d-[0-5]?\d|[0-5]?\d(-[0-5]?\d)?(,[0-5]?\d(-[0-5]?\d)?)+)"

cron_expression = "^%s\s+%s\s+%s\s+%s\s+%s\s+%s$" % (re_year, re_month, re_day, re_hour, re_minute, re_second)

re_range = ".*[-,].*"
re_star = "\*"
re_forward_flash = "/"

min_year, min_month, min_day, min_hour, min_minute, min_second = 1000, 1, 1, 0, 0, 0
max_year, max_month, max_day, max_hour, max_minute, max_second = 10000, 13, 32, 24, 60, 60


class CronError(Exception):

    def __init__(self, error_msg) -> None:
        self.error_msg = error_msg

    def __str__(self) -> str:
        return repr(self.error_msg)


class Cron:

    def __init__(self, cron) -> None:
        now = datetime.datetime.now()

        match = re.match(cron_expression, cron)
        if match is not None:
            years = match.group("years")
            results = self.__resolve(years, now.year, max_year)
            self.__years, self.__period_year, self.__is_star_year = results[0], results[1], results[2]

            months = match.group("months")
            results = self.__resolve(months, now.month, max_month)
            self.__months, self.__period_month, self.__is_star_month = results[0], results[1], results[2]

            days = match.group("days")
            results = self.__resolve(days, now.day, max_day)
            self.__days, self.__period_day, self.__is_star_day = results[0], results[1], results[2]

            hours = match.group("hours")
            results = self.__resolve(hours, now.hour, max_hour)
            self.__hours, self.__period_hour, self.__is_star_hour = results[0], results[1], results[2]

            minutes = match.group("minutes")
            results = self.__resolve(minutes, now.minute, max_minute)
            self.__minutes, self.__period_minute, self.__is_star_minute = results[0], results[1], results[2]

            seconds = match.group("seconds")
            results = self.__resolve(seconds, now.second, max_second)
            self.__seconds, self.__period_second, self.__is_star_second = results[0], results[1], results[2]
        else:
            raise CronError('Cron Expression Format Error')

    def __resolve(self, field_value, default_value, max_value):
        points, period, is_star = None, None, False
        if re.match(re_range, field_value):
            points = self.__flatten(field_value)
        else:
            values = re.split(re_forward_flash, field_value)
            if re.match(re_star, field_value):
                is_star = True
                period = 1 if len(values) == 1 else int(values[1])
                points = [i for i in range(default_value, max_value, period)]
            else:
                period = None if len(values) == 1 else int(values[1])
                if period is None:
                    points = [int(values[0])]
                else:
                    points = [i for i in range(int(values[0]), max_value, period)]
        return (points, period, is_star)

    def __flatten(self, ranges=""):
        values = []
        for r in [re.split("-", i) for i in re.split(",", ranges)]:
            if len(r) == 2:
                values += [i for i in range(int(r[0]), int(r[1]) + 1)]
            elif len(r) == 1:
                values.append(int(r[0]))
        return sorted(set(values))

    def next_execute_time(self):
        for index_year, year in enumerate(self.__years):
            if index_year > 0 and self.__is_star_month:
                self.__months = [i for i in range(min_month, max_month, self.__period_month)]
            for index_month, month in enumerate(self.__months):
                current_max_day = calendar.monthrange(year, month)[1] + 1
                if index_month > 0 and self.__is_star_day:
                    self.__days = [i for i in range(min_day, current_max_day, self.__period_day)]
                days = [i for i in filter(lambda x: x < current_max_day, self.__days)]
                if len(days) == 0:
                    raise StopIteration
                for index_day, day in enumerate(days):
                    if index_day > 0 and self.__is_star_hour:
                        self.__hours = [i for i in range(min_hour, max_hour, self.__period_hour)]
                    for index_hour, hour in enumerate(self.__hours):
                        if index_hour > 0 and self.__is_star_minute:
                            self.__minutes = [i for i in range(min_minute, max_minute, self.__period_minute)]
                        for index_minute, minute in enumerate(self.__minutes):
                            if index_minute > 0 and self.__is_star_second:
                                self.__seconds = [i for i in range(min_second, max_second, self.__period_second)]
                            for second in self.__seconds:
                                yield datetime.datetime(year, month, day, hour, minute, second)
